package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.constants.RedisConstant;
import com.hmdp.dto.Result;
import com.hmdp.entity.RedisData;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.SystemConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.LocaleResolver;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private LocaleResolver localeResolver;

    @Autowired
    private CacheClient cacheClient;


    @Override
//    当前方法是通过逻辑过期解决击穿问题,和互斥锁解决，解决方案全部被封装入到工具类CacheClient中
    public Result queryById(Long id) {

//        Shop shop = cacheClient.queryWithPassThrough(RedisConstant.SHOP_CACHE, id, Shop.class,
//                this::getById, RedisConstant.CACHE_NULL_TTL, TimeUnit.MINUTES);

        Shop shop = cacheClient.queryWithLogicExpire(RedisConstant.SHOP_CACHE, id, Shop.class,
                this::getById, RedisConstant.CACHE_NULL_TTL, TimeUnit.MINUTES);

        if(shop == null){
            return Result.fail("店铺不存在");
        }

        return Result.ok(shop);

    }




//
//
//
////    @Override
////    当前方法是通过逻辑过期解决击穿问题，这是以前的写法，现在需要装配到工具类中
//    public Object queryByIdFormer(Long id) {
//        log.info("开始执行查找店铺过程");
//        String key  = RedisConstant.SHOP_CACHE + id;
////        从redis里面查询商铺缓存
//        String shopJson = stringRedisTemplate.opsForValue().get(key);
//
////  判断是否为空
////        如果不存在，则直接返回fail
//        if (StrUtil.isBlank(shopJson)) {
//            log.info("缓存中不存在shop，返回fail");
//            return Result.fail("店铺不存在");
//        }
////        1.命中，现将json反序列化为对象
//
////        2.判断是否过期
//
////        2.1如果未过期，则返回
//
////        3.如果已过期，则缓存重建
//
////        3.1 获取互斥锁
//
////        3.2 判断是否获取成功
//
////        3.3 获取失败，代表有人正在重建，返回旧的值
//
////        3.4 获取成功
////        4.获取成功，创建独立线程，返回旧值，开始重建缓存
//        String lockKey = RedisConstant.LOCK_SHOP_KEY + id;
//
//        try {
//            RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
//            JSONObject data = (JSONObject)redisData.getData();
//
//            Shop shop = JSONUtil.toBean(data, Shop.class);
//            LocalDateTime expireTime = redisData.getExpireTime();
//
////            2.1如果未过期，则返回
//            if(expireTime.isAfter(LocalDateTime.now())){
//                log.info("缓存未过期，直接返回");
//                return Result.ok(shop);
//            }
//
////            3.如果已过期，则缓存重建
////        3.3 获取失败，代表有人正在重建，返回旧的值
//            boolean isLock = addLock(lockKey);
//            if(!isLock){
//                log.info("缓存已经过期，但是有人正在修改,因为已经上锁了");
//                return Result.ok(shop);
//            }
//
////            获取成功了：
//            log.info("缓存已经过期，开始重建");
//            shopJson = stringRedisTemplate.opsForValue().get(key);
//            redisData = JSONUtil.toBean(shopJson, RedisData.class);
//            data = (JSONObject)redisData.getData();
//
//            shop = JSONUtil.toBean(data, Shop.class);
//            expireTime = redisData.getExpireTime();
//
////            2.1重建 获取锁成功后，进行double check，如果未过期，则返回
//            if(expireTime.isAfter(LocalDateTime.now())){
//                log.info("缓存已更新，直接返回");
//                return Result.ok(shop);
//            }
//
//            CACHE_REBUILD_EXECUTOR.submit(() -> {
////                4.获取成功，创建独立线程，返回旧值，开始重建缓存
//                try {
//                    Thread.sleep(2000);
//                    this.saveShopToRedis(id, 30L);
//                } catch (Exception e) {
//                    throw new RuntimeException(e);
//                } finally {
////                4.1 释放锁
//                    unLock(lockKey);
//                }
//            });
////                4.获取成功，创建独立线程，返回旧值
//            return Result.ok(shop);
//
//        } catch (Exception e) {
//            throw new RuntimeException(e);
//        } finally {
//            unLock(lockKey);
//        }
//    }



//
//    @Override
////    当前方法是通过互斥锁解决击穿问题
//    public Object queryById(Long id) {
//        log.info("开始执行查找店铺过程");
//        String key  = RedisConstant.SHOP_CACHE + id;
////        从redis里面查询商铺缓存
//        String shopJson = stringRedisTemplate.opsForValue().get(key);
//
////  判断是否为空
////        如果存在，则直接返回
//        if (StrUtil.isNotBlank(shopJson)) {
//            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
//            log.info("缓存中存在shop，返回shop");
//            return Result.ok(shop);
//        }
////          如果存在且不blank，但是为空字符串
//        if(shopJson != null){
//            log.info("Redis中有店铺，但是内容为空，防止击穿直接返回");
//            return Result.fail("店铺不存在");
//        }
////        redis未查找到有效店铺，准备加锁并重建redis
//        String lockKey = RedisConstant.LOCK_SHOP_KEY + id;
////         代表当前店铺被加锁了
//        Shop shop = null;
//        try {
//            Boolean isLock = addLock(lockKey);
//            if(!isLock){
//                Thread.sleep(50);
//                return queryById(id);
//            }
////        如果不存在，就去读取数据库
//            shop = getById(id);
////            Thread.sleep(2000);
////        如果数据库不存在，返回错误
//            if (shop == null) {
//                log.info("数据库与缓存中均为查找到shop");
//    //            都查询不到的情况下，同时将redis插入空值
//                stringRedisTemplate.opsForValue().set(key, "", RedisConstant.CACHE_NULL_TTL, TimeUnit.MINUTES);
//                return Result.fail("店铺不存在");
//            }
////        数据库如果存在，写入redis
//            log.info("数据库存在shop信息，写入redis");
//            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), RedisConstant.CACHE_SHOP_TTL + (long)(Math.random() * 10), TimeUnit.MINUTES);
//
////        完成修改了就释放锁
//        } catch (InterruptedException e) {
//            throw new RuntimeException(e);
//        } finally {
//            unLock(lockKey);
//        }
//
//        return Result.ok(shop);
//    }

    @Override
    @Transactional
    public Result updateOneShop(Shop shop) {

        this.updateById(shop);
        if(shop.getId() == null){
            return Result.fail("店铺不存在,或者id不能为空");
        }

        String key  = RedisConstant.SHOP_CACHE + shop.getId();

        stringRedisTemplate.delete(key);

        return Result.ok();
    }

    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
//        判断是否需要坐标查询
        if(x == null || y == null){
            Page<Shop> page = query()
                    .eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            return Result.ok(page.getRecords());
        }

//        计算分页参数
        int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
        String key = RedisConstant.SHOP_GEO_KEY + typeId;


        Circle within = new Circle(new Point(x, y), new Distance(5000));
        RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
                .includeCoordinates()
                .includeDistance()
                .sortAscending()
                .limit(end);


        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = stringRedisTemplate.opsForGeo().radius(key,
                within, args);

        if(geoResults == null){
            return Result.ok(Collections.emptyList());
        }

        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = geoResults.getContent();
        if(content.isEmpty()){
            return Result.ok(Collections.emptyList());
        }
        List<Long> ids = new ArrayList<>();
        HashMap<String, Distance> distanceMap = new HashMap<>();

        content.stream().skip(from).forEach(result -> {
            RedisGeoCommands.GeoLocation<String> location = result.getContent();
            String shopId = location.getName();
            ids.add(Long.valueOf(shopId));
            distanceMap.put(shopId, result.getDistance());
        });
//        查询redis，按照距离排序、分页

//        解析id
        if(ids.isEmpty()){
            return Result.ok(Collections.emptyList());
        }
//        根据id查询出shops
        String idStr = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids).last("order by field(id," + idStr + ")").list();

        shops.forEach(shop -> {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        });

//        返回所有的结果

        return Result.ok(shops);
    }

    public void saveShopToRedis(Long id, Long timeSeconds){
        Shop shop = getById(id);
        String key  = RedisConstant.SHOP_CACHE + id;

        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeSeconds));
        redisData.setData(shop);

        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

//    private boolean addLock(String key){
//        Boolean flag =stringRedisTemplate.opsForValue().setIfAbsent(key, "LOCKING", 10, TimeUnit.SECONDS);
//        return Boolean.TRUE.equals(flag);
//    }
//
//    private void unLock(String key){
//        Boolean flag =stringRedisTemplate.delete(key);
//    }
//
//
//    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


}
